import glob
import io
import itertools
import json
import logging
import os
import re
import subprocess
from collections import namedtuple

from django.conf import settings

from mobsf.MobSF.utils import (
    find_java_binary,
    is_file_exists,
    is_internet_available,
    update_local_db,
)

logger = logging.getLogger(__name__)


class Trackers:
    def __init__(self, apk_dir, tools_dir):
        self.apk = None
        self.apk_dir = apk_dir
        self.tracker_db = os.path.join(
            settings.SIGNATURE_DIR,
            'exodus_trackers')
        self.signatures = None
        self.nb_trackers_signature = 0
        self.compiled_tracker_signature = None
        self.compiled_network_tracker_sig = None
        self.classes = None
        self.tools_dir = tools_dir
        self._update_tracker_db()

    def _update_tracker_db(self):
        """Update Trackers DB."""
        try:
            if not is_internet_available():
                logger.warning('No Internet Connection. '
                               'Skipping Trackers Database Update.')
                return
            exodus_db = '{}/api/trackers'.format(settings.EXODUS_URL)
            resp = update_local_db('Trackers',
                                   exodus_db,
                                   self.tracker_db)
            # Check1: SHA256 Change
            if resp:
                # DB needs update
                # Check2: DB Syntax Changed
                data = json.loads(resp.decode('utf-8', 'ignore'))
                is_db_format_good = False
                if 'trackers' in data:
                    if '1' in data['trackers']:
                        if 'code_signature' in data['trackers']['1']:
                            is_db_format_good = True
                if is_db_format_good:
                    # DB Format is not changed. Let's update DB
                    logger.info('Updating Trackers Database....')
                    with open(self.tracker_db, 'wb') as wfp:
                        wfp.write(resp)
                else:
                    logger.info('Trackers Database format from '
                                'reports.exodus-privacy.eu.org has changed.'
                                ' Database is not updated. '
                                'Please report to: https://github.com/MobSF/'
                                'Mobile-Security-Framework-MobSF/issues')
        except Exception:
            logger.exception('[ERROR] Trackers DB Update')

    def _compile_signatures(self):
        """
        Compile Signatures.

        Compiles the regex associated to each signature, in order to speed up
        the trackers detection.
        :return: A compiled list of signatures.
        """
        self.compiled_tracker_signature = []
        self.compiled_network_tracker_sig = []
        try:
            self.compiled_tracker_signature = [
                re.compile(track.code_signature)
                for track in self.signatures]
            self.compiled_network_tracker_sig = [
                re.compile(track.network_signature)
                for track in self.signatures]
        except TypeError:
            logger.exception('compiling tracker signature failed')

    def load_trackers_signatures(self):
        """
        Load trackers signatures from the official Exodus database.

        :return: a dictionary of signatures.
        """
        self.signatures = []
        with io.open(self.tracker_db,
                     mode='r',
                     encoding='utf8',
                     errors='ignore') as flip:
            data = json.loads(flip.read())
        for elm in data['trackers']:
            self.signatures.append(
                namedtuple('tracker',
                           data['trackers'][elm].keys())(
                    *data['trackers'][elm].values()))
        self._compile_signatures()
        self.nb_trackers_signature = len(self.signatures)

    def get_embedded_classes(self):
        """
        Get the list of Java classes from all DEX files.

        :return: list of Java classes
        """
        if self.classes is not None:
            return self.classes
        for dex_file in glob.iglob(os.path.join(self.apk_dir, '*.dex')):
            if (len(settings.BACKSMALI_BINARY) > 0
                    and is_file_exists(settings.BACKSMALI_BINARY)):
                bs_path = settings.BACKSMALI_BINARY
            else:
                bs_path = os.path.join(self.tools_dir, 'baksmali-2.5.2.jar')
            args = [find_java_binary(), '-jar',
                    bs_path, 'list', 'classes', dex_file]
            classes = subprocess.check_output(
                args, universal_newlines=True).splitlines()
            if self.classes is not None:
                self.classes = self.classes + classes
            else:
                self.classes = classes
        return self.classes

    def detect_trackers_in_list(self, class_list, network=False):
        """
        Detect embedded trackers in the provided classes list/urls.

        :return: list of embedded trackers
        """
        if self.signatures is None:
            self.load_trackers_signatures()

        def _detect_tracker(sig, tracker, class_list):
            for clazz in class_list:
                if sig.search(clazz):
                    return tracker
            return None

        results = []
        if network:
            compiled = self.compiled_network_tracker_sig
            args = [
                (compiled[index], tracker, class_list)
                for (index, tracker) in enumerate(self.signatures) if
                len(tracker.network_signature) > 3]

        else:
            compiled = self.compiled_tracker_signature
            args = [
                (compiled[index], tracker, class_list)
                for (index, tracker) in enumerate(self.signatures) if
                len(tracker.code_signature) > 3]

        for res in itertools.starmap(_detect_tracker, args):
            if res:
                results.append(res)

        trackers = [t for t in results if t is not None]
        trackers = sorted(trackers, key=lambda trackers: trackers.name)
        return trackers

    def detect_trackers(self):
        """
        Detect embedded trackers.

        :return: list of embedded trackers
        """
        if self.signatures is None:
            self.load_trackers_signatures()
        eclasses = self.get_embedded_classes()
        if eclasses:
            return self.detect_trackers_in_list(eclasses)
        return []

    def detect_runtime_trackers(self, items, deps=False):
        """
        Detect runtime trackers.

        :return: list of embedded trackers
        """
        if self.signatures is None:
            self.load_trackers_signatures()
        if items and not deps:
            # Domains
            return self.detect_trackers_in_list(items.keys(), True)
        elif items and deps:
            # Runtime Dependencies
            return self.detect_trackers_in_list(items)
        return []

    def get_trackers(self):
        """Get Trackers."""
        logger.info('Detecting Trackers')
        trackers = self.detect_trackers()
        tracker_dict = {'detected_trackers': len(trackers),
                        'total_trackers': self.nb_trackers_signature,
                        'trackers': []}
        for trk in trackers:
            trk_url = '{}/trackers/{}'.format(settings.EXODUS_URL, trk.id)
            tracker_dict['trackers'].append({
                'name': trk.name,
                'categories': ', '.join(trk.categories),
                'url': trk_url,
            })
        return tracker_dict

    def get_runtime_trackers(self, domains, deps):
        """Get Trackers from Runtime."""
        trackers = []
        logger.info('Detecting Trackers from HTTP(s) traffic')
        network = self.detect_runtime_trackers(domains)
        logger.info('Detecting Trackers from Runtime dependencies')
        runtime = self.detect_runtime_trackers(deps, True)
        for i in runtime:
            if i not in network:
                network.append(i)
        for trk in network:
            trk_url = '{}/trackers/{}'.format(settings.EXODUS_URL, trk.id)
            trackers.append({
                'name': trk.name,
                'categories': ', '.join(trk.categories),
                'url': trk_url,
            })
        return trackers
